from random import random
from elementtree.ElementTree import parse
from math import ceil

pilot = {'atk':2, 'def':2, 'ew':1, 'ld':0}

def roll(num):
   result = 0
   for i in range(num):
      die = ceil(random() * 6)
      if die == 6 and result >= 6:
         result += 1
      elif die > result:
         result = die
   if result == 1:
      result = -10
   return int(result)

class Weapon:
   def __init__(self, name, code, dm, acc, maxRange, rof, melee, indirect, thrown, ae, ai, arc, notes):
      self.name = name
      self.code = code
      self.dm = dm
      self.acc = acc
      if melee:
         self.rng = maxRange,
      elif thrown:
         self.rng = maxRange / 2, maxRange
      else:
         self.rng = int(ceil(int(maxRange)/16.0)), int(maxRange)/8, int(maxRange)/4, int(maxRange)/2, int(maxRange)
      self.rof = rof
      self.melee = melee
      self.indirect = indirect
      self.thrown = thrown
      self.ae = ae
      self.ai = ai
      self.arc = arc
      self.notes = notes
      self.ooa = 0

   def getRangeband(self, distance):
      """Returns which rangeband the weapon is in at distance. 0 is out of range. 1 is pb, 2 is short, and so on."""
      rangeband = 0
      if not distance > self.rng[-1]:
         rangeband += 1
         for i in self.rng:
            if distance > i:
               rangeband += 1
      return rangeband

   def getRangeMod(self, rangeband):
      """Returns the range modifiers for a given rangeband with the weapon. Includes weapon acc"""
      if rangeband == 0:
         rangeMod = -10
      else:
         if self.melee:
            rangeMod = 0
         elif self.thrown:
            if rangeband == 1:
               rangeMod = 0
            else:
               rangeMod = -1
         else:
            if rangeband == 1:
               if self.notes.has_key('mr'):
                  rangeMod = -10
               else:
                  rangeMod = 1
            elif rangeband == 2:
               if self.notes.has_key('mr'):
                  rangeMod = -1
               else:
                  rangeMod = 0
            elif rangeband == 3:
               rangeMod = -1
            elif rangeband == 4:
               if self.notes.has_key('snp'):
                  rangeMod = 0
               else:
                  rangeMod = -2
            else:
               if self.notes.has_key('snp'):
                  rangeMod = -1
               else:
                  rangeMod = -3
      return rangeMod + self.acc

class Gear:
   def __init__(self, name, pilot):
      tree = parse('gearList.xml')
      elem = tree.getroot()
      for i in elem:
         if i.attrib['name'] == name:
            gear = i
            break

      tree = parse('chassisList.xml')
      elem = tree.getroot()
      for i in elem:
         if i.attrib['name'] == gear.attrib['chassis']:
            chassis = i
            break 

      for value in chassis.attrib:
         if value == 'chassis':
            self.chassis = chassis.attrib[value]
         elif value == 'sensors':
            self.sensors = int(chassis.attrib[value])
         elif value == 'detect':
            self.detect = int(chassis.attrib[value])
         elif value == 'comm':
            self.comm = int(chassis.attrib[value])
         elif value == 'autocomm':
            self.autocomm = int(chassis.attrib[value])
         elif value == 'actions':
            self.actions = int(chassis.attrib[value])
         elif value == 'armor':
            self.armor = int(chassis.attrib[value])
         elif value == 'size':
            self.size = int(chassis.attrib[value])
         elif value == 'fc':
            self.fc = int(chassis.attrib[value])
         elif value == 'man':
            self.man = int(chassis.attrib[value])
         
      for value in gear.attrib: # Overrides the generic chassis data with gear specific data
         if value == 'name':
            self.name = gear.attrib[value]
         elif value == 'sensors':
            self.sensors = int(gear.attrib[value])
         elif value == 'detect':
            self.detect = int(gear.attrib[value])
         elif value == 'comm':
            self.comm = int(gear.attrib[value])
         elif value == 'autocomm':
            self.autocomm = int(gear.attrib[value])
         elif value == 'actions':
            self.actions = int(gear.attrib[value])
         elif value == 'armor':
            self.armor = int(gear.attrib[value])
         elif value == 'size':
            self.size = int(gear.attrib[value])
         elif value == 'fc':
            self.fc = int(gear.attrib[value])
         elif value == 'man':
            self.man = int(gear.attrib[value])

      self.movement = {}
      move = chassis.find('movements').getchildren()
      for i in move:
        self.movement[i.attrib['type']] = int(i.attrib['speed'])
      if gear.find('movements'):
         move = gear.find('movements').getchildren()
         for i in move:
            self.movement[i.attrib['type']] = int(i.attrib['speed'])
      
      self.perks = {}
      perk = chassis.find('perks').getchildren()
      for i in perk:
         self.perks[i.attrib['type']] = int(i.attrib['value'])
      if gear.find('perks'):
         perk = gear.find('perks').getchildren()
         for i in perk:
            self.perks[i.attrib['type']] = int(i.attrib['value'])
      markedForDel = {}
      for i in self.perks: # Cleans up the perks list
         if self.perks[i] == 0:
            markedForDel[i] = 1
      for i in markedForDel:
         self.perks.pop(i)

      self.weapons = {}
      
      weapons = gear.find('defaultWeapons').getchildren()
      for weapon in weapons:
         self.addWeapon(weapon.attrib['code'])

         for value in weapon.attrib: # This overrides the default weapon values with gear specific ones.
            if value == 'rof':
               self.weapons[weapon.attrib['code']].rof = int(weapon.attrib[value])
            elif value == 'arc':
               self.weapons[weapon.attrib['code']].arc = weapon.attrib[value]
            else:
               try:
                  self.weapons[weapon.attrib['code']].notes[value] = int(weapon.attrib[value])
               except:
                  pass
      self.addWeapon('physical')

      self.pilot = pilot

      self.pos = [0, 0]
      self.damage = 0
      self.defaultArmor = self.armor
      self.stunned = 0
      self.speedband = 0
      if self.movement.has_key('Ground'):
         self.movementMode = 'Ground'
      else:
         self.movementMode = 'Walker'

      self.unspentActions = self.actions
      self.firedWeapons = []
      self.designated = {}
      self.fo = ()
      return

   def getAttackMod(self):
      """Returns ranged attack modifier based on speed and fire control"""
      rangeMod = 0
      if self.speedband == 0:
         rangeMod += 2
      elif self.speedband == 2:
         rangeMod -= 3
      return rangeMod + self.fc

   def getBoxes(self):
      """Total number of danage "boxes" a gear has left"""
      boxes = 4
      if self.perks.has_key('sturdy'):
         boxes += self.perks['sturdy']
      if self.perks.has_key('field armor'):
         boxes += self.perks['field armor']
      if self.perks.has_key('fragile'):
         boxes -= self.perks['fragile']
      return boxes - self.damage
   
   def isDead(self):
      """Is the unit dead?"""
      if self.getBoxes() <= 0:
         return 1

   def getPenelty(self):
      """Returns the modifier for damage"""
      penelty = 0
      boxes = self.getBoxes()
      if self.perks.has_key('fragile'):
         if boxes == 1:
            penelty = -1
      else:
         if boxes == 1:
            penelty = -2
         elif boxes == 2:
            penelty = -1
      return penelty
              
   def getDodge(self):
      """Returns the modifier for speed based on the Gear's current movement mode. Include maneuver ."""
      if self.speedband == 0:
         dodge = -3
      else:
         speed = self.movement[self.movementMode]
         if self.speedband == 1:
            speed = int(ceil(speed/2.0))
         if speed < 3:
            dodge = -2
         elif speed < 5:
            dodge = -1
         elif speed < 7:
            dodge = 0
         elif speed < 10:
            dodge = 1
         else:
            dodge = 2
      return dodge + self.man

   def addWeapon(self, code):
      tree = parse('weaponList.xml')
      elem = tree.getroot()
      
      for values in elem:
         if values.attrib['code'] == code:
            # Setting some default values that may not be included in the XML.
            if self.weapons.has_key(code):
               self.weapons[code].notes['count'] = 2
               if self.weapons[code].notes.has_key('ammo'):
                  self.weapons[code].notes['ammo'] *= 2                        
            else:                                      
               arc = 'f'
               acc = 0
               rof = 0
               ae = 0
               melee = 0
               indirect = 0
               thrown = 0
               ai = 0
               notes = {}
               for value in values.attrib:
                  if value == 'name':
                     name = values.attrib[value]
                  elif value == 'ae':
                     ae = int(values.attrib[value])
                  elif value == 'ai':
                     ai = int(values.attrib[value])
                  elif value == 'm':
                     melee = int(values.attrib[value])
                  elif value == 'rof':
                     rof = int(values.attrib[value])
                  elif value == 'if':
                     indirect = int(values.attrib[value])
                  elif value == 't':
                     thrown = int(values.attrib[value])
                  elif value == 'acc':
                     acc = int(values.attrib[value])
                  elif value == 'range':
                     if values.attrib[value] == 't':
                        maxRange = self.size * 2
                     else:
                        maxRange = int(values.attrib[value])
                  elif value == 'dm':
                     if values.attrib[value].find('size') > -1:
                        if values.attrib[value].find('+') > -1:
                           dm = self.size + int(values.attrib[value].split('+')[1])
                        else:
                           dm = self.size
                     else:
                        dm = int(values.attrib[value])
                  else:
                     try:
                        notes[value] = int(values.attrib[value])
                     except:
                        pass
               self.weapons[code] = Weapon(name, code, dm, acc, maxRange, rof, melee, indirect, thrown, ae, ai, arc, notes)
                                           
   def removeWeapon(self, code):
      self.weapons.pop(code)

   def meleeAttack(self, tabletop, target, weapon):
      self.unspentActions -= 1
      defender = tabletop.getTargets(0, target)[0]
      weapon = self.weapons[weapon]

      # Get the attack dice
      atkDice = self.pilot['def']
      if weapon.code == 'VK':
         if self.weapons.has_key('VB'):
            atkDice += 1
      atkRoll = roll(atkDice)

      # Get the attack modifiers
      atkMod = self.getPenelty() + weapon.acc
      if self.speedband > defender.speedband:
         atkMod += 1
      if self.size > defender.size:
         atkMod += 1

      # Get the defence dice
      defDice = defender.pilot['def']
      defRoll = roll(defDice)

      # Get the defence modifiers
      defMod = defender.getPenelty()
      if defender.size > self.size:
         defMod += 1
      if defender.stunned:
         if defender.getDodge() > 0:
            defMod -= 1
      for w in defender.weapons:
         if defender.weapons[w].melee:
            if defender.weapons[w].acc > 0:
               defMod += 1
               break

      # Total defence
      defTotal = defDice + defMod
      if defTotal < 0:
         defTotal = 0

      # Get MoS
      mos = atkRoll + atkMod - defTotal
      if weapon.notes.has_key('ap'):
         if mos >= 0:
            mos += 2

      # Find and deal damage
      if defender.perks.has_key('reinforced armor front'):
         damage = mos * weapon.dm / (defender.armor + self.perks('reinforced armor front'))
      else:
         damage = mos * weapon.dm / defender.armor
      if damage >= 3:
         damage = 10
      elif damage < 0:
         damage = 0
      if weapon.notes.has_key('ac'):
         if damage > 0:
            defender.armor -= mos        
      defender.damage += damage

      return {defender.id:(damage, mos)}

   def rangedAttack(self, tabletop, target, weapon, burst=0, spray=0, ae=0, indirect=0, linked=0, undetected=0, cover=1, priority=0, crossfire=0, ai=0):
      # Making fire() more rules pure. Now handles multiple targets!
      # target is an (x, y) location.
      # Need to do something better for crossfire, priority target.
      # Need to add facings
      # Need to check for unspent actions before firing? Disallow repeated firing? rof at ranges? Out of ammo? Range?
      self.unspentActions -= 1
      weapon = self.weapons[weapon]
      self.firedWeapons.append(weapon.code)
      results = {}

      # Set up the attack roll.
      atkDice = self.pilot['atk']
      if self.perks.has_key('defective fire control'):
         atkDice -= self.perks['defective fire control']
      atkRoll = roll(atkDice)

      # Set up the attack modifiers.
      rangeband = weapon.getRangeband(tabletop.getDistance(target, self.pos))
      atkMod = self.getPenelty() + self.getAttackMod() + weapon.getRangeMod(rangeband) + cover
      if weapon.notes.has_key('g'):
         for td in self.designated:
            if target == td:
               atkMod += 1

      # Adjust the damage modifier.
      dm = weapon.dm
      dm += burst
      if weapon.notes.has_key('mb'):
         mb = (rangeband - 2) * weapon.notes['mb']
         if mb < 0:
            mb = 0
         dm -= mb
      if ae:
         if not weapon.ai:
            dm -= ae

      # Reduce ammo if limited ammo.
      if weapon.notes.has_key('ammo'):
         weapon.ammo -= 1
         if weapon.ammo == 0:
            weapon.ooa = 1

      # Check to see if weapon runs out of ammo from burst
      rof = burst + spray
      if atkRoll <= 0:
         weapon.ooa = 1
      if not weapon.notes.has_key('fgc'):
         elif atkRoll <= 2:
            if rof < weapon.rof / 2:
               weapon.ooa = 1
         elif atkRoll == 3:
            if weapon.rof == 1 and rof == 1:
               weapon.ooa = 1
            elif rof >= weapon.rof / 2 and rof < weapon.rof:
               weapon.ooa = 1
         elif atkRoll == 4:
            if weapon.rof == rof:
               weapon.ooa = 1

      # Scatter if indirect. Collect targets
      if indirect or weapon.thrown:
         if atkRoll + atkMod <= 4:
            scatter = 4 - atkRoll - atkMod + (random() * 6) - self.sensors
            if weapon.thrown:
               scatter *= 0.5
         target = tabletop.getScatter(scatter, target)
         defenders = tabletop.getTargets(spray + ae, target)

      # Direct fire needs to hit before ae and rof are applied. Stupid rule.
      else:
         defenders = tabletop.getTargets(0, target)

         # If linked, all targets have to defend twice.
         if linked:
            defenders *= 2

         # Initial direct fire combat.
         for defender in defenders:
            results[defender.id] = defender.rangedDefence(atkRoll, atkMod, weapon, dm, indirect=indirect, undetected=undetected, priority=priority)
            if ae or spray:
               if results[defender.id][1] > 0:
                  if ae:
                     if defender.perks.has_key('reinforced armor front'):
                        if dm * 2 > defender.armor + defender.perks['reinforced armor front']:
                           defender.stunned = 1
                     else:
                        if dm * 2 > defender.armor:
                           defender.stunned = 1
                  defenders = tabletop.getTargets(ae + spray, target)
            for i in defenders:
               if defender == i:
                  defenders.pop(defenders.index(i))
            
                        
      # Initial indirect fire or AE direct fire combat. Very much the same as the last one.
      if linked:
         defenders *= 2

      for defender in defenders:
         results[defender.id] = defender.rangedDefence(atkRoll, atkMod, weapon, dm, indirect=indirect, undetected=undetected, priority=priority)
         if ae:
            if defender.perks.has_key('reinforced armor front'):
               if dm * 2 > defender.armor + defender.perks['reinforced armor front']:
                  defender.stunned = 1
            else:
               if dm * 2 > defender.armor:
                  defender.stunned = 1
      return results

   def rangedDefence(self, atkRoll, atkMod, weapon, dm, indirect=0, undetected=0, priority=0):
      # Set up the defence roll.
      defDice = self.pilot['def']
      if weapon.ai:
         if not self.perks.has_key('Infantry'):
            defDice += 1
      defRoll = roll(defDice)

      # Set up the defence modifiers.
      defMod = self.getPenelty() + self.getDodge() - indirect - undetected - priority

      # Combine defence roll and modifer.
      defTotal = defRoll + defMod
      if defTotal < 0:
         defTotal = 0

      # Get MoS.
      if indirect:
         mos = atkRoll - defTotal
      else:
         mos = atkRoll + atkMod - defTotal
      if weapon.notes.has_key('ap'):
         if mos >= 0:
            mos += 2
      if mos < 0:
         mos = 0

      # Find and deal damage.
      if self.perks.has_key('reinforced armor front'):
         damage = mos * dm / (self.armor + self.perks('reinforced armor front'))
      else:
         damage = mos * dm / self.armor
      if damage >= 3:
         damage = 10
      elif damage < 0:
         damage = 0
      if weapon.notes.has_key('ac'):
         if damage > 0:
            self.armor -= mos            
      if weapon.notes.has_key('h'):
         die = ceil(random() * 6)
         if self.perks.has_key('vuln to haywire'):
            if die >= 3:
               damage += 1
            else:
               self.stunned = 1
         else:
            if die == 6:
               damage += 1
            elif die >= 3:
               self.stunned = 1
      self.damage += damage
      return damage, mos 

   def untap(self):
      """Call this at the start of the turn"""
      self.unspentActions = self.actions
      self.firedWeapons = []
      self.designated = {}
      self.fo = ()
